package pl.edu.agh.neurony.logic;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import pl.edu.agh.neurony.exceptions.BadNeuronInitiationException;
import pl.edu.agh.neurony.exceptions.LayerConstructionException;
import pl.edu.agh.neurony.exceptions.LearningException;
import pl.edu.agh.neurony.exceptions.NeuronNetException;
import pl.edu.agh.neurony.exceptions.ProhibitedOperationException;
import pl.edu.agh.neurony.logic.activationFunctions.ActivationFunc;

public class Net {

	// singleton section
	protected Net() {
	}

	private static Net instance = null;

	public static synchronized Net getInstance() {
		if (instance == null) {
			instance = new Net();
		}
		return instance;
	}

	public void clear() {
		inputs.clear();
		outputLayer.neurons.clear();
		layersMap.clear();
	}

	// fields
	public List<InputNeuron> inputs = new ArrayList<InputNeuron>();
	public GrossbergLayer outputLayer;
	public Map<Integer, Layer> layersMap = new HashMap<Integer, Layer>();

	private List<List<Double>> inputsValues;

	public void setInputsValues(List<List<Double>> inputsValues) {
		this.inputsValues = inputsValues;
	}

	public List<List<Double>> getInputsValues() {
		return inputsValues;
	}

	public List<List<Double>> desiredOutputs = new ArrayList<List<Double>>();

	// TODO: to bym wykasowal i uzywal metody dodajacej od razu calosc. Patrz
	// jak ja to zrobilem
	public void addDesiredOutput(Double[] values) {
		List<Double> l = new ArrayList<Double>(Arrays.asList(values));
		desiredOutputs.add(l);
	}

	// private methods
	private void connectWithNeighbourLayers(int layerNr, Neuron neuron) {
		if (layerNr == 1) {
			for (Neuron inputNeuron : inputs)
				inputNeuron.connectOutputNeuron(neuron);
		} else {
			Layer lowerLayer = layersMap.get(layerNr - 1);
			lowerLayer.createOutputConnectionsToNeuron(neuron);
		}

		int lastLayer = layersMap.values().size();
		if (layerNr == lastLayer) {
			outputLayer.createInputConnectionsToNeuron(neuron);
		} else {
			Layer higherLayer = layersMap.get(layerNr + 1);
			higherLayer.createInputConnectionsToNeuron(neuron);
		}
	}

	private void addNeuronToLayer(int layerNr, Neuron neuron, boolean connectWithNeighbours)
			throws LayerConstructionException {
		if (connectWithNeighbours)
			connectWithNeighbourLayers(layerNr, neuron);
		layersMap.get(layerNr).addNeuron(neuron);
	}

	public void addInputNeuron(InputNeuron inputNeuron, boolean connectWithNeighbours) {
		inputs.add(inputNeuron);
		if (connectWithNeighbours) {
			Layer firstLayer = layersMap.get(1);
			if (firstLayer == null)
				outputLayer.createInputConnectionsToNeuron(inputNeuron);
			else
				firstLayer.createInputConnectionsToNeuron(inputNeuron);
		}
	}

	public void addOutputNeuron(OutputNeuron outputNeuron, boolean connectWithNeighbours)
			throws LayerConstructionException {
		if (connectWithNeighbours) {
			Layer lastLayer = layersMap.get(size() - 2);
			if (lastLayer == null)
				for (Neuron inputNeuron : inputs)
					inputNeuron.connectOutputNeuron(outputNeuron);
			else
				lastLayer.createOutputConnectionsToNeuron(outputNeuron);
		}
		outputLayer.addNeuron(outputNeuron);
	}

	// public methods
	public void initWithOneInputAndOneOutput(InputNeuron inputNeuron, OutputNeuron outputNeuron)
			throws NeuronNetException {
		Connection connection = new Connection(inputNeuron, outputNeuron);
		inputNeuron.connectOutputNeuron(outputNeuron, connection);

		inputs.add(inputNeuron);
		outputLayer = new GrossbergLayer(Layer.OUTPUT_LAYER, outputNeuron);

	}

	public void addNeuron(int layerNr, Neuron neuron, boolean connectWithNeighbours) throws LayerConstructionException {
		if (layerNr != Layer.INPUT_LAYER && layerNr != Layer.OUTPUT_LAYER)
			addNeuronToLayer(layerNr, neuron, connectWithNeighbours);
		else if (layerNr == Layer.INPUT_LAYER) {
			if (neuron instanceof InputNeuron)
				addInputNeuron((InputNeuron) neuron, true);
			else
				throw new LayerConstructionException("only input neurons can be added to input layer");
		} else if (layerNr == Layer.OUTPUT_LAYER) {
			if (neuron instanceof OutputNeuron)
				addOutputNeuron((OutputNeuron) neuron, true);
			else
				throw new LayerConstructionException("only output neurons can be added to output layer");
		}
	}

	public Neuron addNeuron(int layerNr) throws LayerConstructionException {
		Neuron neuron = new Neuron(layerNr);
		if (layerNr == Layer.INPUT_LAYER)
			neuron = new InputNeuron(layerNr);
		else if (layerNr == Layer.OUTPUT_LAYER)
			neuron = new OutputNeuron(layerNr);
		addNeuron(layerNr, neuron, true);
		return neuron;
	}

	public Layer addLayer(Neuron neuron) throws LayerConstructionException {
		int layerNr = layersMap.values().size() + 1;
		int lowerLayerNr = layerNr - 1;
		Layer layer = new GrossbergLayer(layerNr, neuron);
		layersMap.put(layerNr, layer);
		if (lowerLayerNr == Layer.INPUT_LAYER) {
			for (Neuron inputNeuron : inputs)
				for (Connection connection : inputNeuron.getOutputConnections())
					connection.disconnect();
		} else {
			layersMap.get(lowerLayerNr).disconnectFromUpperLayer();
		}
		connectWithNeighbourLayers(layerNr, neuron);

		return layer;
	}

	public Layer addLayer(Neuron neuron, boolean ownConnections) throws NeuronNetException {
		int layerNr = layersMap.values().size() + 1;
		Layer layer = new GrossbergLayer(layerNr, neuron);
		layersMap.put(layerNr, layer);

		return layer;
	}

	public Neuron addLayer() throws NeuronNetException {
		Neuron neuron = new Neuron(layersMap.keySet().size() + 1);
		addLayer(neuron);
		return neuron;
	}

	public Layer addKohonenLayer(Neuron neuron, int neighborhood, double learningRatio, int learningSteps,
			double conscienceFactor) throws LayerConstructionException {
		int layerNr = layersMap.values().size() + 1;
		int lowerLayerNr = layerNr - 1;
		KohonenLayer layer = new KohonenLayer(layerNr, neuron, neighborhood, learningRatio, learningSteps,
				conscienceFactor);
		layersMap.put(layerNr, layer);
		if (lowerLayerNr == Layer.INPUT_LAYER) {
			for (Neuron inputNeuron : inputs)
				for (Connection connection : inputNeuron.getOutputConnections())
					connection.disconnect();
		} else {
			layersMap.get(lowerLayerNr).disconnectFromUpperLayer();
		}
		connectWithNeighbourLayers(layerNr, neuron);

		return layer;
	}

	public Neuron addKohonenLayer(Neuron neuron) throws LayerConstructionException {
		int layerNr = layersMap.values().size() + 1;
		KohonenLayer layer = new KohonenLayer(layerNr, neuron);
		layersMap.put(layerNr, layer);

		return neuron;
	}

	public Neuron addKohonenLayer() throws LayerConstructionException {
		Neuron neuron = new Neuron(layersMap.keySet().size() + 1);
		int layerNr = layersMap.values().size() + 1;
		int lowerLayerNr = layerNr - 1;
		KohonenLayer layer = new KohonenLayer(layerNr, neuron);
		layersMap.put(layerNr, layer);
		if (lowerLayerNr == Layer.INPUT_LAYER) {
			for (Neuron inputNeuron : inputs)
				for (Connection connection : inputNeuron.getOutputConnections())
					connection.disconnect();
		} else {
			layersMap.get(lowerLayerNr).disconnectFromUpperLayer();
		}
		connectWithNeighbourLayers(layerNr, neuron);
		return neuron;
	}

	public Neuron addKohonenLayer(int neighborhood, double learningRatio, int learningSteps, double conscienceFactor)
			throws NeuronNetException {
		Neuron neuron = new Neuron(layersMap.keySet().size() + 1);
		addKohonenLayer(neuron, neighborhood, learningRatio, learningSteps, conscienceFactor);
		return neuron;
	}

	public Neuron removeLastNeuronInLayer(int layerNr) throws ProhibitedOperationException {
		Neuron neuron = null;
		if (layerNr == Layer.INPUT_LAYER) {
			if (inputs.size() < 2)
				throw new ProhibitedOperationException("Input layer has to have at least one neuron");

			neuron = inputs.remove(inputs.size() - 1);
			for (Connection connection : neuron.getOutputConnections())
				connection.disconnect();
		} else if (layerNr == Layer.OUTPUT_LAYER) {
			outputLayer.removeLastNeuron();
		} else {
			neuron = layersMap.get(layerNr).removeLastNeuron();
		}

		return neuron;
	}

	public List<Neuron> removeLayer(int layerNr) throws ProhibitedOperationException {
		if (layerNr == Layer.INPUT_LAYER)
			throw new ProhibitedOperationException("You cannot remove input layer");
		if (layerNr == Layer.OUTPUT_LAYER)
			throw new ProhibitedOperationException("You cannot remove output layer");

		List<Neuron> neuronsToRemove = layersMap.get(layerNr).disconnectFromLowerLayer();
		layersMap.get(layerNr).disconnectFromUpperLayer();
		Layer upperLayer = layersMap.get(layerNr + 1);
		Layer lowerLayer = layersMap.get(layerNr - 1);
		if (upperLayer == null) {
			upperLayer = outputLayer;
		}
		if (lowerLayer != null) {
			lowerLayer.connectWithUpperLayer(upperLayer);
		} else if (lowerLayer == null) {
			for (Neuron inputNeuron : inputs)
				upperLayer.createInputConnectionsToNeuron(inputNeuron);
		}
		layersMap.remove(layerNr);
		return neuronsToRemove;
	}

	public void fillInputsWithData(List<Double> inputData) {
		for (int i = 0; i < inputs.size(); i++)
			inputs.get(i).output = inputData.get(i);
	}

	public void fillLayerWithBiases(int layer, List<Double> values) {
		if (layer == Layer.OUTPUT_LAYER) {
			outputLayer.fillLayerWithValues(values);
		} else {
			layersMap.get(layer).fillLayerWithValues(values);
		}
	}

	public void fillInputNeuronWithSignal(int inputNeuronNr, double signal) {
		inputs.get(inputNeuronNr).output = signal;
	}

	public int sizeOfLayer(int layerNr) {
		if (layerNr == Layer.INPUT_LAYER)
			return inputs.size();
		if (layerNr == Layer.OUTPUT_LAYER)
			return outputLayer.size();

		Layer layer = layersMap.get(layerNr);
		return layer == null ? 0 : layer.size();
	}

	public int size() {
		int input = inputs.size() > 0 ? 1 : 0;
		int output = outputLayer != null ? 1 : 0;
		return layersMap.keySet().size() + input + output;
	}

	public boolean hasOutput() {
		return outputLayer.size() > 0;
	}

	public boolean isValid() {
		for (Neuron neuron : inputs)
			if (!neuron.isValid())
				return false;
		if (!outputLayer.isValid())
			return false;
		for (Layer layer : layersMap.values())
			if (!layer.isValid())
				return false;

		return true;
	}

	public List<Double> stimulate() throws BadNeuronInitiationException {
		List<Double> output = new ArrayList<Double>();
		if (isValid())
			for (Neuron outputNeuron : outputLayer.getNeurons()) {
				Double calculatedOutput = outputNeuron.calculateOutput();
				output.add(calculatedOutput);
			}
		else
			return null;

		return output;
	}

	public void resetValues(boolean resetOutputs, boolean resetErrors) {
		outputLayer.resetValues(resetOutputs, resetErrors);
		for (Layer l : layersMap.values()) {
			l.resetValues(resetOutputs, resetErrors);
		}
	}

	public void assignRandomWeights(double from, double to) {
		outputLayer.assignRandomWeights(from, to);
		for (Layer l : layersMap.values()) {

			l.assignRandomWeights(from, to);

		}
	}

	public void setActivationFunctionForLayer(int outputLayerNr, ActivationFunc activationFunc) {
		if (outputLayerNr == Layer.OUTPUT_LAYER) {
			for (Neuron n : outputLayer.getNeurons()) {
				n.setActivationFunction(activationFunc);
			}
		} else {
			Layer l = layersMap.get(outputLayerNr);
			for (Neuron n : l.getNeurons()) {
				n.setActivationFunction(activationFunc);
			}
		}

	}

	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("Net\n");
		for (Layer l : layersMap.values()) {
			sb.append(l.toString());
		}

		return sb.toString();
	}

	public void setLearningParams(int neighborhood, double basicLearningFactor, double conscienceFactor,
			int neighborhoodSize, Double momentum) {

		for (Layer l : layersMap.values()) {
			l.setLearningParams(basicLearningFactor, momentum, conscienceFactor, neighborhood, neighborhoodSize);
		}
		outputLayer.setLearningParams(basicLearningFactor, momentum, conscienceFactor, neighborhood, neighborhoodSize);
	}

	public void setWeights() {
		for (Layer l : layersMap.values()) {
			if (l instanceof KohonenLayer) {
				KohonenLayer kl = (KohonenLayer) l;
				kl.setOutputs();
			}
		}
	}

	public void learnBackProp(int learningSteps) throws BadNeuronInitiationException, LearningException {
		for (int i = 0; i < learningSteps; i++) {
			System.out.println("step " + i);
			for (int k = 0; k < desiredOutputs.size(); k++) {
				resetValues(true, true);
				setInputs(k);
				List<Double> output = stimulate();
				calculateError(output, k);
				int layerNr = inputs.get(0).getOutputConnections().get(0).getOutputNeuron().layerNr;
				Layer layer = layersMap.get(layerNr);
				if (layer != null)
					layer.learn(i + 1, learningSteps);

				outputLayer.learn(i + 1, learningSteps);
				displayLearnSummary(output, k);
			}
		}
		System.out.println("done!");
		displayLastLayerWeight();
	}
	
	private void displayLastLayerWeight() {
		for (Neuron neuron : outputLayer.getNeurons()) {
			for (Connection conn : neuron.getInputConnections())
				System.out.print(conn.getWeight() + " ");
			System.out.println(" BIAS: " + neuron.bias);
		}
	}

	private void displayLearnSummary(List<Double> output, int k) {
		for (int i = 0; i < output.size(); i++) {
			double error = desiredOutputs.get(k).get(i) - output.get(i);
			System.out.println(output.get(i) + "(" + error + ")");
		}
	}

	private void setInputs(int k) {
		for (int i = 0; i < inputs.size(); i++) {
			double signal = inputsValues.get(k).get(i);
			inputs.get(i).output = signal;
		}
	}

	private void calculateError(List<Double> output, int inputNumber) throws LearningException {
		List<Neuron> outputs = outputLayer.getNeurons();

		for (int j = 0; j < outputs.size(); j++) {
			Neuron outputNeuron = outputs.get(j);
			double desiredOutput = desiredOutputs.get(inputNumber).get(j);
			double calculatedOutput = output.get(j);
			double error = desiredOutput - calculatedOutput;
			outputNeuron.error = error * outputNeuron.activationFunction.derivative(outputNeuron.input);
		}
		Neuron middleLayerNeuron = outputs.get(0).getInputConnections().get(0).getInputNeuron();
		GrossbergLayer layer = (GrossbergLayer) layersMap.get(middleLayerNeuron.layerNr);
		if (layer != null)
			layer.propagateErrorBackward();
	}

	public void learnCounterProp(Integer learnType, int learningSteps) throws BadNeuronInitiationException {
		for (int i = 0; i < layersMap.size(); i++) {
			layersMap.get(i).learn(0, learningSteps);
		}
		for (int i = 0; i < learningSteps; i++)
			outputLayer.learn(i, learningSteps);
	}

	public String displayInfo() {
		String info = "Net:\n";
		info += "\tLayer " + Layer.INPUT_LAYER + "\n";
		for (Neuron neuron : inputs) {
			info += "\t\t" + neuron.displayInfo();
		}
		info += "\n";

		for (Layer layer : layersMap.values())
			info += layer.displayInfo();

		info += "\tLayer " + Layer.OUTPUT_LAYER + "\n";
		for (Neuron neuron : outputLayer.getNeurons()) {
			info += "\t\t" + neuron.displayInfo();
		}
		info += "\n";

		return info;
	}

	public void test() throws BadNeuronInitiationException {
		if (inputsValues != null) {
			for (List<Double> input : inputsValues) {
				System.out.println("NEW INPUT !!!!!!!!!! ");

				fillInputsWithData(input);
				for (Layer l : layersMap.values()) {
					if (l instanceof KohonenLayer) {
						KohonenLayer kl = (KohonenLayer) l;
						kl.setOutputs();
					}
				}
				for (Neuron n : outputLayer.getNeurons()) {
					System.out.println("==== Neuron ====");
					for (Connection c : n.getInputConnections()) {
						System.out.println("connection " + c.getWeight());
					}
					n.resetOutputValue();
					n.calculateOutput();

					System.out.println(n.output);
					System.out.println();

				}
			}
		}

	}

	public void setDesiredOutput(List<List<Double>> subList) {
		desiredOutputs = subList;
	}
}
